using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Globalization;

namespace LeisureCenter.MetadataHandlers
{
    class Mp3MetadataHandler : AudioMetadataHandler
    {
        private long dataOffset;

        /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        /// <summary>
        /// Return the genre as a string associated with a integer.
        /// </summary>
        /// <param name="genre"> The genre to return as a string. </param>
        /// <returns> The genre as a string. </returns>
        /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        public static string GetGenre(int genre)
        {
            string[] genres = {	"Blues",
								"Classic Rock",
								"Country",
								"Dance",
								"Disco",
								"Funk",
								"Grunge",
								"Hip-Hop",
								"Jazz",
								"Metal",
								"New Age",
								"Oldies",
								"Other",
								"Pop",
								"R&B",
								"Rap",
								"Reggae",
								"Rock",
								"Techno",
								"Industrial",
								"Alternative",
								"Ska",
								"Death Metal",
								"Pranks",
								"Soundtrack",
								"Euro-Techno",
								"Ambient",
								"Trip-Hop",
								"Vocal",
								"Jazz+Funk",
								"Fusion",
								"Trance",
								"Classical",
								"Instrumental",
								"Acid",
								"House",
								"Game",
								"Sound Clip",
								"Gospel",
								"Noise",
								"Alt. Rock",
								"Bass",
								"Soul",
								"Punk",
								"Space",
								"Meditative",
								"Instrumental Pop",
								"Instrumental Rock",
								"Ethnic",
								"Gothic",
								"Darkwave",
								"Techno-Industrial",
								"Electronic",
								"Pop-Folk",
								"Eurodance",
								"Dream",
								"Southern Rock",
								"Comedy",
								"Cult",
								"Gangsta Rap",
								"Top 40",
								"Christian Rap",
								"Pop/Funk",
								"Jungle",
								"Native American",
								"Cabaret",
								"New Wave",
								"Psychedelic",
								"Rave",
								"Showtunes",
								"Trailer",
								"Lo-Fi",
								"Tribal",
								"Acid Punk",
								"Acid Jazz",
								"Polka",
								"Retro",
								"Musical",
								"Rock & Roll",
								"Hard Rock",
								"Folk",
								"Folk/Rock",
								"National Folk",
								"Swing",
								"Fast-Fusion",
								"Bebob",
								"Latin",
								"Revival",
								"Celtic",
								"Bluegrass",
								"Avantgarde",
								"Gothic Rock",
								"Progressive Rock",
								"Psychedelic Rock",
								"Symphonic Rock",
								"SlowRock",
								"Big Band",
								"Chorus",
								"Easy Listening",
								"Acoustic",
								"Humour",
								"Speech",
								"Chanson",
								"Opera",
								"Chamber Music",
								"Sonata",
								"Symphony",
								"Booty Bass",
								"Primus",
								"Porn Groove",
								"Satire",
								"Slow Jam",
								"Club",
								"Tango",
								"Samba",
								"Folklore",
								"Ballad",
								"Power Ballad",
								"Rhythmic Soul",
								"Freestyle",
								"Duet",
								"Punk Rock",
								"DrumSolo",
								"A Cappella",
								"Euro-House",
								"Dance Hall",
								"Goa",
								"Drum & Bass",
								"Club-House",
								"Hardcore",
								"Terror",
								"Indie",
								"BritPop",
								"Negerpunk",
								"Polsk Punk",
								"Beat",
								"Christian Gangsta Rap",
								"Heavy Metal",
								"Black Metal",
								"Crossover",
								"Contemporary Christian",
								"Christian Rock", 
								"Merengue", 
								"Salsa", 
								"Thrash Metal",
								"Anime", 
								"JPop",
								"Synthpop"  
							};

            if (genre > genres.Length) return "";
            return genres[genre];
        }

        private bool ReadID3v1Tag(Stream stream)
        {
            byte[] b = new byte[128];

            stream.Seek(-128, SeekOrigin.End);
            stream.Read(b, 0, 128);

            Encoding instEncoding = Encoding.Default;	// Encoding is an Abstract class
            string id3Tag = instEncoding.GetString(b);		// Convert the Byte Array to a String

            // If there is an attached ID3 v1.x TAG then read it 
            if (id3Tag.Substring(0, 3).ToUpper() == "TAG")
            {
                title = id3Tag.Substring(3, 30).Trim().Trim('\0');
                artist = id3Tag.Substring(33, 30).Trim().Trim('\0');
                album = id3Tag.Substring(63, 30).Trim().Trim('\0');
                try
                {
                    year = int.Parse(id3Tag.Substring(93, 4).Trim().Trim('\0'));
                }
                catch
                {
                    year = null;
                }
                //_id3Comment = id3Tag.Substring(97, 28).Trim();

                // Get the track number if TAG conforms to ID3 v1.1
                if (id3Tag[125] == 0)
                {
                    track = b[126];
                }
                else
                {
                    track = null;
                }

                genre = GetGenre(b[127]);
                return true;
            }
            else return false;
        }

        private bool ReadID3v2Tag(Stream stream)
        {
            string valTag = "";			// The value we are searching for
            int f = 1;
            byte[] b = new byte[4];		// The data we are reading
            int[] nb = new int[4];

            stream.Position = 0;
            stream.Read(b, 0, 3);			// Read the first three byte
            Encoding instEncoding = new ASCIIEncoding();
            string filetype = instEncoding.GetString(b);	// Transform the byte into a string
            // If this string begin with ID3 we have Id3v2
            if (filetype.StartsWith("ID3"))
            {
                string flag = "";
                int i = 0;

                stream.Seek(6, SeekOrigin.Begin);
                stream.Read(b, 0, 4);

                nb[3] = b[3] | ((b[2] & 1) << 7);
                nb[2] = ((b[2] >> 1) & 63) | ((b[1] & 3) << 6);
                nb[1] = ((b[1] >> 2) & 31) | ((b[0] & 7) << 5);
                nb[0] = ((b[0] >> 3) & 15);

                ulong FrameSize = 10 + (ulong)nb[3] | ((ulong)nb[2] << 8) | ((ulong)nb[1] << 16) | ((ulong)nb[0] << 24);
                this.dataOffset = Convert.ToInt32(FrameSize);

                while (i < this.dataOffset && f > 0)
                {
                    i += 10;
                    stream.Seek(i, SeekOrigin.Begin);
                    stream.Read(b, 0, 4);
                    flag = instEncoding.GetString(b);
                    stream.Seek(i + 4, SeekOrigin.Begin);
                    stream.Read(b, 0, 4);

                    try
                    {
                        f = Convert.ToInt32(((uint)b[0] << 24) + ((uint)b[1] << 16) + ((uint)b[2] << 8) + ((uint)b[3]));
                        if (f > 256 * 256)
                            return false;
                    }
                    catch (Exception ex)
                    {
                        Trace.TraceError(string.Format("An {0} occured reading ID3v2 tag : {1}", ex.GetType().Name, ex.Message));
                        return false;
                    }

                    if (f == 0)
                        continue;
                    byte[] s = new byte[f];
                    stream.Seek(i + 10, SeekOrigin.Begin);
                    stream.Read(s, 0, f);
                    valTag = "";
                    Decoder d;
                    switch (s[0])
                    {
                        case 0x01:
                            d = Encoding.Unicode.GetDecoder();
                            break;
                        case 0x02:
                            d = Encoding.BigEndianUnicode.GetDecoder();
                            break;
                        case 0x03:
                            d = Encoding.UTF8.GetDecoder();
                            break;
                        default:
                            d = Encoding.GetEncoding("ISO-8859-1").GetDecoder();
                            break;
                    }
                    char[] chars = new Char[d.GetCharCount(s, 1, f - 1)];
                    int charLen = d.GetChars(s, 1, s.Length - 1, chars, 0);
                    for (int j = 0; j < chars.Length; j++)
                    {
                        if (chars[j] >= 0x20 && chars[j] != 0xFFFE && chars[j] != 0xFEFF)
                            valTag += chars[j].ToString(CultureInfo.InvariantCulture);
                    }
                    valTag = valTag.Trim();

                    if (!string.IsNullOrEmpty(valTag))
                    {
                        // Search for the correct tag
                        switch (flag)
                        {
                            case "TENC":	// Encoded
                                //this._id3EncodedBy = valTag.Trim();
                                break;
                            case "WXXX":	// URL
                                //this._id3URL = valTag.Trim();
                                break;
                            case "TCOP":	// Copyright
                                //this._id3Copyright = valTag.Trim();
                                break;
                            case "TOPE":	// Org Artist
                                //this._id3OrgArtist = valTag.Trim();
                                break;
                            case "TCOM":	// Composer
                                //this._id3Composer = valTag.Trim();
                                break;
                            case "COMM":	// Comment
                                //this._id3Comment = valTag.Trim();
                                break;
                            case "TYER":	// Year
                                try
                                {
                                    year = int.Parse(valTag);
                                }
                                catch
                                {
                                    Trace.TraceWarning(@"{0} : Flag '{1}' value '{2}' could not be parsed !", this.GetType().Name, flag, valTag);
                                    year = null;
                                }
                                break;
                            case "TIT2":	// Titre
                                title = valTag;
                                break;
                            case "TRCK":	// TrackNumber
                                try
                                {
                                    track = Convert.ToByte(valTag);
                                }
                                catch
                                {
                                    Trace.TraceWarning(@"{0} : Flag '{1}' value '{2}' could not be parsed !", this.GetType().Name, flag, valTag);
                                    track = null;
                                }
                                break;
                            case "TPE1":	// Artist
                                artist = valTag;
                                break;
                            case "TALB":	// Album
                                album = valTag;
                                break;
                            case "TCON":	// Genre
                                try
                                {
                                    if (valTag[0] != '(')
                                        genre = valTag;
                                    else
                                    if (valTag.StartsWith(@"(("))
                                        genre = valTag.Substring(1);
                                    else
                                    {
                                        valTag = valTag.Substring(1, valTag.IndexOf(')') - 1);
                                        genre = GetGenre(Convert.ToByte(valTag));
                                    }
                                }
                                catch
                                {
                                    Trace.TraceWarning(@"{0} : Flag '{1}' value '{2}' could not be parsed !", this.GetType().Name, flag, valTag);
                                    genre = null;
                                }
                                break;
                            default:
                                Trace.TraceWarning(@"Unrecognized tag {0} with value {1}", flag, valTag);
                                break;
                        }
                    }
                    i += f;
                }
                return true;
            }
            else return false;
        }

        public override void ReadMetadata(string fileName, Stream stream, bool computeDataHash)
        {
            this.dataOffset = 0;

            bool hasID3v1 = ReadID3v1Tag(stream);
            ReadID3v2Tag(stream);

            if (computeDataHash)
            {
                stream.Position = this.dataOffset;
                if (hasID3v1)
                {
                    this.dataHash = HashStreamFromPositionLength(stream, stream.Length - 128);
                }
                else
                {
                    this.dataHash = HashStreamFromPosition(stream);
                }
            }
        }
    }

    class Mp3MetadataHandlerFactory : MetadataHandlerFactory
    {
        public override bool AcceptMedia(string filename, Stream stream)
        {
            if (stream.Length > 0)
            {
                BinaryReader br = new BinaryReader(stream);
                byte[] buffer = br.ReadBytes(3);
                return (buffer[0] == 'I' && buffer[1] == 'D' && buffer[2] == '3')
                        || (buffer[0] == 0xFF && buffer[1] == 0xFB);
            }
            return false;
        }

        public override MetadataHandler CreateMetadata()
        {
            return new Mp3MetadataHandler();
        }
    }
}
